/**
 * @file
 * SNMP message processing (RFC1157).
 */

/*
 * Copyright (c) 2006 Axon Digital Design B.V., The Netherlands.
 * Copyright (c) 2016 Elias Oenal.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
 * SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 *
 * Author: Christiaan Simons <christiaan.simons@axon.tv>
 *         Martin Hentschel <info@cl-soft.de>
 *         Elias Oenal <lwip@eliasoenal.com>
 */

#include "lwip/apps/snmp_opts.h"

#if LWIP_SNMP /* don't build if not configured for use in lwipopts.h */

#include "snmp_msg.h"
#include "snmp_asn1.h"
#include "snmp_core_priv.h"
#include "lwip/ip_addr.h"
#include "lwip/stats.h"

#if LWIP_SNMP_V3
	#include "lwip/apps/snmpv3.h"
	#include "snmpv3_priv.h"
	#ifdef LWIP_SNMPV3_INCLUDE_ENGINE
		#include LWIP_SNMPV3_INCLUDE_ENGINE
	#endif
#endif

#include <string.h>

/* public (non-static) constants */
/** SNMP community string */
const char* snmp_community = SNMP_COMMUNITY;
/** SNMP community string for write access */
const char* snmp_community_write = SNMP_COMMUNITY_WRITE;
/** SNMP community string for sending traps */
const char* snmp_community_trap = SNMP_COMMUNITY_TRAP;

snmp_write_callback_fct snmp_write_callback     = NULL;
void*                   snmp_write_callback_arg = NULL;

/**
 * @ingroup snmp_core
 * Returns current SNMP community string.
 * @return current SNMP community string
 */
const char*
snmp_get_community(void)
{
	return snmp_community;
}

/**
 * @ingroup snmp_core
 * Sets SNMP community string.
 * The string itself (its storage) must be valid throughout the whole life of
 * program (or until it is changed to sth else).
 *
 * @param community is a pointer to new community string
 */
void
snmp_set_community(const char* const community)
{
	LWIP_ASSERT("community string is too long!", strlen(community) <= SNMP_MAX_COMMUNITY_STR_LEN);
	snmp_community = community;
}

/**
 * @ingroup snmp_core
 * Returns current SNMP write-access community string.
 * @return current SNMP write-access community string
 */
const char*
snmp_get_community_write(void)
{
	return snmp_community_write;
}

/**
 * @ingroup snmp_traps
 * Returns current SNMP community string used for sending traps.
 * @return current SNMP community string used for sending traps
 */
const char*
snmp_get_community_trap(void)
{
	return snmp_community_trap;
}

/**
 * @ingroup snmp_core
 * Sets SNMP community string for write-access.
 * The string itself (its storage) must be valid throughout the whole life of
 * program (or until it is changed to sth else).
 *
 * @param community is a pointer to new write-access community string
 */
void
snmp_set_community_write(const char* const community)
{
	LWIP_ASSERT("community string must not be NULL", community != NULL);
	LWIP_ASSERT("community string is too long!", strlen(community) <= SNMP_MAX_COMMUNITY_STR_LEN);
	snmp_community_write = community;
}

/**
 * @ingroup snmp_traps
 * Sets SNMP community string used for sending traps.
 * The string itself (its storage) must be valid throughout the whole life of
 * program (or until it is changed to sth else).
 *
 * @param community is a pointer to new trap community string
 */
void
snmp_set_community_trap(const char* const community)
{
	LWIP_ASSERT("community string is too long!", strlen(community) <= SNMP_MAX_COMMUNITY_STR_LEN);
	snmp_community_trap = community;
}

/**
 * @ingroup snmp_core
 * Callback fired on every successful write access
 */
void
snmp_set_write_callback(snmp_write_callback_fct write_callback, void* callback_arg)
{
	snmp_write_callback     = write_callback;
	snmp_write_callback_arg = callback_arg;
}

/* ----------------------------------------------------------------------- */
/* forward declarations */
/* ----------------------------------------------------------------------- */

static err_t snmp_process_get_request(struct snmp_request* request);
static err_t snmp_process_getnext_request(struct snmp_request* request);
static err_t snmp_process_getbulk_request(struct snmp_request* request);
static err_t snmp_process_set_request(struct snmp_request* request);

static err_t snmp_parse_inbound_frame(struct snmp_request* request);
static err_t snmp_prepare_outbound_frame(struct snmp_request* request);
static err_t snmp_complete_outbound_frame(struct snmp_request* request);
static void snmp_execute_write_callbacks(struct snmp_request* request);


/* ----------------------------------------------------------------------- */
/* implementation */
/* ----------------------------------------------------------------------- */

void
snmp_receive(void* handle, struct pbuf* p, const ip_addr_t* source_ip, u16_t port)
{
	err_t err;
	struct snmp_request request;

	memset(&request, 0, sizeof(request));
	request.handle       = handle;
	request.source_ip    = source_ip;
	request.source_port  = port;
	request.inbound_pbuf = p;

	snmp_stats.inpkts++;

	err = snmp_parse_inbound_frame(&request);

	if(err == ERR_OK) {
		err = snmp_prepare_outbound_frame(&request);

		if(err == ERR_OK) {

			if(request.error_status == SNMP_ERR_NOERROR) {
				/* only process frame if we do not already have an error to return (e.g. all readonly) */
				if(request.request_type == SNMP_ASN1_CONTEXT_PDU_GET_REQ) {
					err = snmp_process_get_request(&request);
				} else if(request.request_type == SNMP_ASN1_CONTEXT_PDU_GET_NEXT_REQ) {
					err = snmp_process_getnext_request(&request);
				} else if(request.request_type == SNMP_ASN1_CONTEXT_PDU_GET_BULK_REQ) {
					err = snmp_process_getbulk_request(&request);
				} else if(request.request_type == SNMP_ASN1_CONTEXT_PDU_SET_REQ) {
					err = snmp_process_set_request(&request);
				}
			}

			if(err == ERR_OK) {
				err = snmp_complete_outbound_frame(&request);

				if(err == ERR_OK) {
					err = snmp_sendto(request.handle, request.outbound_pbuf, request.source_ip, request.source_port);

					if((request.request_type == SNMP_ASN1_CONTEXT_PDU_SET_REQ)
					        && (request.error_status == SNMP_ERR_NOERROR)
					        && (snmp_write_callback != NULL)) {
						/* raise write notification for all written objects */
						snmp_execute_write_callbacks(&request);
					}
				}
			}
		}

		if(request.outbound_pbuf != NULL) {
			pbuf_free(request.outbound_pbuf);
		}
	}
}

static u8_t
snmp_msg_getnext_validate_node_inst(struct snmp_node_instance* node_instance, void* validate_arg)
{
	if(((node_instance->access & SNMP_NODE_INSTANCE_ACCESS_READ) != SNMP_NODE_INSTANCE_ACCESS_READ) || (node_instance->get_value == NULL)) {
		return SNMP_ERR_NOSUCHINSTANCE;
	}

	if((node_instance->asn1_type == SNMP_ASN1_TYPE_COUNTER64) && (((struct snmp_request*)validate_arg)->version == SNMP_VERSION_1)) {
		/* according to RFC 2089 skip Counter64 objects in GetNext requests from v1 clients */
		return SNMP_ERR_NOSUCHINSTANCE;
	}

	return SNMP_ERR_NOERROR;
}

static void
snmp_process_varbind(struct snmp_request* request, struct snmp_varbind* vb, u8_t get_next)
{
	err_t err;
	struct snmp_node_instance node_instance;
	memset(&node_instance, 0, sizeof(node_instance));

	if(get_next) {
		struct snmp_obj_id result_oid;
		request->error_status = snmp_get_next_node_instance_from_oid(vb->oid.id, vb->oid.len, snmp_msg_getnext_validate_node_inst, request,  &result_oid, &node_instance);

		if(request->error_status == SNMP_ERR_NOERROR) {
			snmp_oid_assign(&vb->oid, result_oid.id, result_oid.len);
		}
	} else {
		request->error_status = snmp_get_node_instance_from_oid(vb->oid.id, vb->oid.len, &node_instance);

		if(request->error_status == SNMP_ERR_NOERROR) {
			/* use 'getnext_validate' method for validation to avoid code duplication (some checks have to be executed here) */
			request->error_status = snmp_msg_getnext_validate_node_inst(&node_instance, request);

			if(request->error_status != SNMP_ERR_NOERROR) {
				if(node_instance.release_instance != NULL) {
					node_instance.release_instance(&node_instance);
				}
			}
		}
	}

	if(request->error_status != SNMP_ERR_NOERROR)  {
		if(request->error_status >= SNMP_VARBIND_EXCEPTION_OFFSET) {
			if((request->version == SNMP_VERSION_2c) || request->version == SNMP_VERSION_3) {
				/* in SNMP v2c a varbind related exception is stored in varbind and not in frame header */
				vb->type = (SNMP_ASN1_CONTENTTYPE_PRIMITIVE | SNMP_ASN1_CLASS_CONTEXT | (request->error_status & SNMP_VARBIND_EXCEPTION_MASK));
				vb->value_len = 0;

				err = snmp_append_outbound_varbind(&(request->outbound_pbuf_stream), vb);

				if(err == ERR_OK) {
					/* we stored the exception in varbind -> go on */
					request->error_status = SNMP_ERR_NOERROR;
				} else if(err == ERR_BUF) {
					request->error_status = SNMP_ERR_TOOBIG;
				} else {
					request->error_status = SNMP_ERR_GENERROR;
				}
			}
		} else {
			/* according to RFC 1157/1905, all other errors only return genError */
			request->error_status = SNMP_ERR_GENERROR;
		}
	} else {
		s16_t len = node_instance.get_value(&node_instance, vb->value);
		vb->type = node_instance.asn1_type;

		if(len >= 0) {
			vb->value_len = (u16_t)len; /* cast is OK because we checked >= 0 above */

			LWIP_ASSERT("SNMP_MAX_VALUE_SIZE is configured too low", (vb->value_len & ~SNMP_GET_VALUE_RAW_DATA) <= SNMP_MAX_VALUE_SIZE);
			err = snmp_append_outbound_varbind(&request->outbound_pbuf_stream, vb);

			if(err == ERR_BUF) {
				request->error_status = SNMP_ERR_TOOBIG;
			} else if(err != ERR_OK) {
				request->error_status = SNMP_ERR_GENERROR;
			}
		} else {
			request->error_status = SNMP_ERR_GENERROR;
		}

		if(node_instance.release_instance != NULL) {
			node_instance.release_instance(&node_instance);
		}
	}
}


/**
 * Service an internal or external event for SNMP GET.
 *
 * @param request points to the associated message process state
 */
static err_t
snmp_process_get_request(struct snmp_request* request)
{
	snmp_vb_enumerator_err_t err;
	struct snmp_varbind vb;
	vb.value = request->value_buffer;

	LWIP_DEBUGF(SNMP_DEBUG, ("SNMP get request\n"));

	while(request->error_status == SNMP_ERR_NOERROR) {
		err = snmp_vb_enumerator_get_next(&request->inbound_varbind_enumerator, &vb);

		if(err == SNMP_VB_ENUMERATOR_ERR_OK) {
			if((vb.type == SNMP_ASN1_TYPE_NULL) && (vb.value_len == 0)) {
				snmp_process_varbind(request, &vb, 0);
			} else {
				request->error_status = SNMP_ERR_GENERROR;
			}
		} else if(err == SNMP_VB_ENUMERATOR_ERR_EOVB) {
			/* no more varbinds in request */
			break;
		} else if(err == SNMP_VB_ENUMERATOR_ERR_ASN1ERROR) {
			/* malformed ASN.1, don't answer */
			return ERR_ARG;
		} else {
			request->error_status = SNMP_ERR_GENERROR;
		}
	}

	return ERR_OK;
}

/**
 * Service an internal or external event for SNMP GET.
 *
 * @param request points to the associated message process state
 */
static err_t
snmp_process_getnext_request(struct snmp_request* request)
{
	snmp_vb_enumerator_err_t err;
	struct snmp_varbind vb;
	vb.value = request->value_buffer;

	LWIP_DEBUGF(SNMP_DEBUG, ("SNMP get-next request\n"));

	while(request->error_status == SNMP_ERR_NOERROR) {
		err = snmp_vb_enumerator_get_next(&request->inbound_varbind_enumerator, &vb);

		if(err == SNMP_VB_ENUMERATOR_ERR_OK) {
			if((vb.type == SNMP_ASN1_TYPE_NULL) && (vb.value_len == 0)) {
				snmp_process_varbind(request, &vb, 1);
			} else {
				request->error_status = SNMP_ERR_GENERROR;
			}
		} else if(err == SNMP_VB_ENUMERATOR_ERR_EOVB) {
			/* no more varbinds in request */
			break;
		} else if(err == SNMP_VB_ENUMERATOR_ERR_ASN1ERROR) {
			/* malformed ASN.1, don't answer */
			return ERR_ARG;
		} else {
			request->error_status = SNMP_ERR_GENERROR;
		}
	}

	return ERR_OK;
}

/**
 * Service an internal or external event for SNMP GETBULKT.
 *
 * @param request points to the associated message process state
 */
static err_t
snmp_process_getbulk_request(struct snmp_request* request)
{
	snmp_vb_enumerator_err_t err;
	s32_t non_repeaters     = request->non_repeaters;
	s32_t repetitions;
	u16_t repetition_offset = 0;
	struct snmp_varbind_enumerator repetition_varbind_enumerator;
	struct snmp_varbind vb;
	vb.value = request->value_buffer;

	if(SNMP_LWIP_GETBULK_MAX_REPETITIONS > 0) {
		repetitions = LWIP_MIN(request->max_repetitions, SNMP_LWIP_GETBULK_MAX_REPETITIONS);
	} else {
		repetitions = request->max_repetitions;
	}

	LWIP_DEBUGF(SNMP_DEBUG, ("SNMP get-bulk request\n"));

	/* process non repeaters and first repetition */
	while(request->error_status == SNMP_ERR_NOERROR) {
		if(non_repeaters == 0) {
			repetition_offset = request->outbound_pbuf_stream.offset;

			if(repetitions == 0) {
				/* do not resolve repeaters when repetitions is set to 0 */
				break;
			}

			repetitions--;
		}

		err = snmp_vb_enumerator_get_next(&request->inbound_varbind_enumerator, &vb);

		if(err == SNMP_VB_ENUMERATOR_ERR_EOVB) {
			/* no more varbinds in request */
			break;
		} else if(err == SNMP_VB_ENUMERATOR_ERR_ASN1ERROR) {
			/* malformed ASN.1, don't answer */
			return ERR_ARG;
		} else if((err != SNMP_VB_ENUMERATOR_ERR_OK) || (vb.type != SNMP_ASN1_TYPE_NULL) || (vb.value_len != 0)) {
			request->error_status = SNMP_ERR_GENERROR;
		} else {
			snmp_process_varbind(request, &vb, 1);
			non_repeaters--;
		}
	}

	/* process repetitions > 1 */
	while((request->error_status == SNMP_ERR_NOERROR) && (repetitions > 0) && (request->outbound_pbuf_stream.offset != repetition_offset)) {

		u8_t all_endofmibview = 1;

		snmp_vb_enumerator_init(&repetition_varbind_enumerator, request->outbound_pbuf, repetition_offset, request->outbound_pbuf_stream.offset - repetition_offset);
		repetition_offset = request->outbound_pbuf_stream.offset; /* for next loop */

		while(request->error_status == SNMP_ERR_NOERROR) {
			vb.value = NULL; /* do NOT decode value (we enumerate outbound buffer here, so all varbinds have values assigned) */
			err = snmp_vb_enumerator_get_next(&repetition_varbind_enumerator, &vb);

			if(err == SNMP_VB_ENUMERATOR_ERR_OK) {
				vb.value = request->value_buffer;
				snmp_process_varbind(request, &vb, 1);

				if(request->error_status != SNMP_ERR_NOERROR) {
					/* already set correct error-index (here it cannot be taken from inbound varbind enumerator) */
					request->error_index = request->non_repeaters + repetition_varbind_enumerator.varbind_count;
				} else if(vb.type != (SNMP_ASN1_CONTENTTYPE_PRIMITIVE | SNMP_ASN1_CLASS_CONTEXT | SNMP_ASN1_CONTEXT_VARBIND_END_OF_MIB_VIEW)) {
					all_endofmibview = 0;
				}
			} else if(err == SNMP_VB_ENUMERATOR_ERR_EOVB) {
				/* no more varbinds in request */
				break;
			} else {
				LWIP_DEBUGF(SNMP_DEBUG, ("Very strange, we cannot parse the varbind output that we created just before!"));
				request->error_status = SNMP_ERR_GENERROR;
				request->error_index  = request->non_repeaters + repetition_varbind_enumerator.varbind_count;
			}
		}

		if((request->error_status == SNMP_ERR_NOERROR) && all_endofmibview) {
			/* stop when all varbinds in a loop return EndOfMibView */
			break;
		}

		repetitions--;
	}

	if(request->error_status == SNMP_ERR_TOOBIG) {
		/* for GetBulk it is ok, if not all requested variables fit into the response -> just return the varbinds added so far */
		request->error_status = SNMP_ERR_NOERROR;
	}

	return ERR_OK;
}

/**
 * Service an internal or external event for SNMP SET.
 *
 * @param request points to the associated message process state
 */
static err_t
snmp_process_set_request(struct snmp_request* request)
{
	snmp_vb_enumerator_err_t err;
	struct snmp_varbind vb;
	vb.value = request->value_buffer;

	LWIP_DEBUGF(SNMP_DEBUG, ("SNMP set request\n"));

	/* perform set test on all objects */
	while(request->error_status == SNMP_ERR_NOERROR) {
		err = snmp_vb_enumerator_get_next(&request->inbound_varbind_enumerator, &vb);

		if(err == SNMP_VB_ENUMERATOR_ERR_OK) {
			struct snmp_node_instance node_instance;
			memset(&node_instance, 0, sizeof(node_instance));

			request->error_status = snmp_get_node_instance_from_oid(vb.oid.id, vb.oid.len, &node_instance);

			if(request->error_status == SNMP_ERR_NOERROR) {
				if(node_instance.asn1_type != vb.type) {
					request->error_status = SNMP_ERR_WRONGTYPE;
				} else if(((node_instance.access & SNMP_NODE_INSTANCE_ACCESS_WRITE) != SNMP_NODE_INSTANCE_ACCESS_WRITE) || (node_instance.set_value == NULL)) {
					request->error_status = SNMP_ERR_NOTWRITABLE;
				} else {
					if(node_instance.set_test != NULL) {
						request->error_status = node_instance.set_test(&node_instance, vb.value_len, vb.value);
					}
				}

				if(node_instance.release_instance != NULL) {
					node_instance.release_instance(&node_instance);
				}
			}
		} else if(err == SNMP_VB_ENUMERATOR_ERR_EOVB) {
			/* no more varbinds in request */
			break;
		} else if(err == SNMP_VB_ENUMERATOR_ERR_INVALIDLENGTH) {
			request->error_status = SNMP_ERR_WRONGLENGTH;
		} else if(err == SNMP_VB_ENUMERATOR_ERR_ASN1ERROR) {
			/* malformed ASN.1, don't answer */
			return ERR_ARG;
		} else {
			request->error_status = SNMP_ERR_GENERROR;
		}
	}

	/* perform real set operation on all objects */
	if(request->error_status == SNMP_ERR_NOERROR) {
		snmp_vb_enumerator_init(&request->inbound_varbind_enumerator, request->inbound_pbuf, request->inbound_varbind_offset, request->inbound_varbind_len);

		while(request->error_status == SNMP_ERR_NOERROR) {
			err = snmp_vb_enumerator_get_next(&request->inbound_varbind_enumerator, &vb);

			if(err == SNMP_VB_ENUMERATOR_ERR_OK) {
				struct snmp_node_instance node_instance;
				memset(&node_instance, 0, sizeof(node_instance));
				request->error_status = snmp_get_node_instance_from_oid(vb.oid.id, vb.oid.len, &node_instance);

				if(request->error_status == SNMP_ERR_NOERROR) {
					if(node_instance.set_value(&node_instance, vb.value_len, vb.value) != SNMP_ERR_NOERROR) {
						if(request->inbound_varbind_enumerator.varbind_count == 1) {
							request->error_status = SNMP_ERR_COMMITFAILED;
						} else {
							/* we cannot undo the set operations done so far */
							request->error_status = SNMP_ERR_UNDOFAILED;
						}
					}

					if(node_instance.release_instance != NULL) {
						node_instance.release_instance(&node_instance);
					}
				}
			} else if(err == SNMP_VB_ENUMERATOR_ERR_EOVB) {
				/* no more varbinds in request */
				break;
			} else {
				/* first time enumerating varbinds work but second time not, although nothing should have changed in between ??? */
				request->error_status = SNMP_ERR_GENERROR;
			}
		}
	}

	return ERR_OK;
}

#define PARSE_EXEC(code, retValue) \
  if ((code) != ERR_OK) { \
    LWIP_DEBUGF(SNMP_DEBUG, ("Malformed ASN.1 detected.\n")); \
    snmp_stats.inasnparseerrs++; \
    return retValue; \
  }

#define PARSE_ASSERT(cond, retValue) \
  if (!(cond)) { \
    LWIP_DEBUGF(SNMP_DEBUG, ("SNMP parse assertion failed!: " # cond)); \
    snmp_stats.inasnparseerrs++; \
    return retValue; \
  }

#define BUILD_EXEC(code, retValue) \
  if ((code) != ERR_OK) { \
    LWIP_DEBUGF(SNMP_DEBUG, ("SNMP error during creation of outbound frame!: " # code)); \
    return retValue; \
  }

#define IF_PARSE_EXEC(code)   PARSE_EXEC(code, ERR_ARG)
#define IF_PARSE_ASSERT(code) PARSE_ASSERT(code, ERR_ARG)

/**
 * Checks and decodes incoming SNMP message header, logs header errors.
 *
 * @param request points to the current message request state return
 * @return
 * - ERR_OK SNMP header is sane and accepted
 * - ERR_VAL SNMP header is either malformed or rejected
 */
static err_t
snmp_parse_inbound_frame(struct snmp_request* request)
{
	struct snmp_pbuf_stream pbuf_stream;
	struct snmp_asn1_tlv tlv;
	s32_t parent_tlv_value_len;
	s32_t s32_value;
	err_t err;

	IF_PARSE_EXEC(snmp_pbuf_stream_init(&pbuf_stream, request->inbound_pbuf, 0, request->inbound_pbuf->tot_len));

	/* decode main container consisting of version, community and PDU */
	IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
	IF_PARSE_ASSERT((tlv.type == SNMP_ASN1_TYPE_SEQUENCE) && (tlv.value_len == pbuf_stream.length));
	parent_tlv_value_len = tlv.value_len;

	/* decode version */
	IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
	IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_INTEGER);
	parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
	IF_PARSE_ASSERT(parent_tlv_value_len > 0);

	IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &s32_value));

	if((s32_value != SNMP_VERSION_1) &&
	        (s32_value != SNMP_VERSION_2c)
#if LWIP_SNMP_V3
	        && (s32_value != SNMP_VERSION_3)
#endif
	  ) {
		/* unsupported SNMP version */
		snmp_stats.inbadversions++;
		return ERR_ARG;
	}

	request->version = (u8_t)s32_value;

#if LWIP_SNMP_V3

	if(request->version == SNMP_VERSION_3) {
		u16_t u16_value;
		u16_t inbound_msgAuthenticationParameters_offset;

		/* SNMPv3 doesn't use communities */
		/* @todo: Differentiate read/write access */
		strcpy((char*)request->community, snmp_community);
		request->community_strlen = (u16_t)strlen(snmp_community);

		/* RFC3414 globalData */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_SEQUENCE);
		parent_tlv_value_len -= SNMP_ASN1_TLV_HDR_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		/* decode msgID */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_INTEGER);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &s32_value));
		request->msg_id = s32_value;

		/* decode msgMaxSize */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_INTEGER);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &s32_value));
		request->msg_max_size = s32_value;

		/* decode msgFlags */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &s32_value));
		request->msg_flags = (u8_t)s32_value;

		/* decode msgSecurityModel */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_INTEGER);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &s32_value));
		request->msg_security_model = s32_value;

		/* RFC3414 msgSecurityParameters
		 * The User-based Security Model defines the contents of the OCTET
		 * STRING as a SEQUENCE.
		 *
		 * We skip the protective dummy OCTET STRING header
		 * to access the SEQUENCE header.
		 */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
		parent_tlv_value_len -= SNMP_ASN1_TLV_HDR_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		/* msgSecurityParameters SEQUENCE header */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_SEQUENCE);
		parent_tlv_value_len -= SNMP_ASN1_TLV_HDR_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		/* decode msgAuthoritativeEngineID */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		IF_PARSE_EXEC(snmp_asn1_dec_raw(&pbuf_stream, tlv.value_len, request->msg_authoritative_engine_id,
		                                &u16_value, SNMP_V3_MAX_ENGINE_ID_LENGTH));
		request->msg_authoritative_engine_id_len = (u8_t)u16_value;

		/* msgAuthoritativeEngineBoots */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_INTEGER);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);
		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &request->msg_authoritative_engine_boots));

		/* msgAuthoritativeEngineTime */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_INTEGER);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);
		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &request->msg_authoritative_engine_time));
		/* @todo: Implement time window checking */

		/* msgUserName */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		IF_PARSE_EXEC(snmp_asn1_dec_raw(&pbuf_stream, tlv.value_len, request->msg_user_name,
		                                &u16_value, SNMP_V3_MAX_USER_LENGTH));
		request->msg_user_name_len = (u8_t)u16_value;
		/* @todo: Implement unknown user error response */
		IF_PARSE_EXEC(snmpv3_get_user((char*)request->msg_user_name, NULL, NULL, NULL, NULL));

		/* msgAuthenticationParameters */
		memset(request->msg_authentication_parameters, 0, SNMP_V3_MAX_AUTH_PARAM_LENGTH);
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);
		/* Remember position */
		inbound_msgAuthenticationParameters_offset = pbuf_stream.offset;
		LWIP_UNUSED_ARG(inbound_msgAuthenticationParameters_offset);
		/* Read auth parameters */
		IF_PARSE_ASSERT(tlv.value_len <= SNMP_V3_MAX_AUTH_PARAM_LENGTH);
		IF_PARSE_EXEC(snmp_asn1_dec_raw(&pbuf_stream, tlv.value_len, request->msg_authentication_parameters,
		                                &u16_value, tlv.value_len));

#if LWIP_SNMP_V3_CRYPTO

		if(request->msg_flags & SNMP_V3_AUTH_FLAG) {
			const u8_t zero_arr[SNMP_V3_MAX_AUTH_PARAM_LENGTH] = { 0 };
			u8_t key[20];
			u8_t algo;
			u8_t hmac[LWIP_MAX(SNMP_V3_SHA_LEN, SNMP_V3_MD5_LEN)];
			struct snmp_pbuf_stream auth_stream;

			/* Rewind stream */
			IF_PARSE_EXEC(snmp_pbuf_stream_init(&pbuf_stream, request->inbound_pbuf, 0, request->inbound_pbuf->tot_len));
			IF_PARSE_EXEC(snmp_pbuf_stream_seek_abs(&pbuf_stream, inbound_msgAuthenticationParameters_offset));
			/* Set auth parameters to zero for verification */
			IF_PARSE_EXEC(snmp_asn1_enc_raw(&pbuf_stream, zero_arr, tlv.value_len));

			/* Verify authentication */
			IF_PARSE_EXEC(snmp_pbuf_stream_init(&auth_stream, request->inbound_pbuf, 0, request->inbound_pbuf->tot_len));

			IF_PARSE_EXEC(snmpv3_get_user((char*)request->msg_user_name, &algo, key, NULL, NULL));
			IF_PARSE_EXEC(snmpv3_auth(&auth_stream, request->inbound_pbuf->tot_len, key, algo, hmac));
			/* @todo: Implement error response */
			IF_PARSE_EXEC(memcmp(request->msg_authentication_parameters, hmac, SNMP_V3_MAX_AUTH_PARAM_LENGTH));
		}

#else
		/* Ungraceful exit if we encounter cryptography and don't support it.
		 * @todo: Implement error response
		 */
		IF_PARSE_ASSERT(!(request->msg_flags & (SNMP_V3_AUTH_FLAG | SNMP_V3_PRIV_FLAG)));
#endif

		/* msgPrivacyParameters */
		memset(request->msg_privacy_parameters, 0, SNMP_V3_MAX_PRIV_PARAM_LENGTH);
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		IF_PARSE_EXEC(snmp_asn1_dec_raw(&pbuf_stream, tlv.value_len, request->msg_privacy_parameters,
		                                &u16_value, SNMP_V3_MAX_PRIV_PARAM_LENGTH));

#if LWIP_SNMP_V3_CRYPTO

		/* Decrypt message */
		if(request->msg_flags & SNMP_V3_PRIV_FLAG) {
			u8_t key[20];
			u8_t algo;

			IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
			IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
			parent_tlv_value_len -= SNMP_ASN1_TLV_HDR_LENGTH(tlv);
			IF_PARSE_ASSERT(parent_tlv_value_len > 0);

			IF_PARSE_EXEC(snmpv3_get_user((char*)request->msg_user_name, NULL, NULL, &algo, key));
			IF_PARSE_EXEC(snmpv3_crypt(&pbuf_stream, tlv.value_len, key,
			                           request->msg_privacy_parameters, request->msg_authoritative_engine_boots,
			                           request->msg_authoritative_engine_time, algo, SNMP_V3_PRIV_MODE_DECRYPT));
		}

#endif

		/* Scoped PDU
		 * Encryption context
		 */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_SEQUENCE);
		parent_tlv_value_len -= SNMP_ASN1_TLV_HDR_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		/* contextEngineID */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		IF_PARSE_EXEC(snmp_asn1_dec_raw(&pbuf_stream, tlv.value_len, request->context_engine_id,
		                                &u16_value, SNMP_V3_MAX_ENGINE_ID_LENGTH));
		request->context_engine_id_len = (u8_t)u16_value;

		/* contextName */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		IF_PARSE_EXEC(snmp_asn1_dec_raw(&pbuf_stream, tlv.value_len, request->context_name,
		                                &u16_value, SNMP_V3_MAX_ENGINE_ID_LENGTH));
		request->context_name_len = (u8_t)u16_value;
	} else
#endif
	{
		/* decode community */
		IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
		IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_OCTET_STRING);
		parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
		IF_PARSE_ASSERT(parent_tlv_value_len > 0);

		err = snmp_asn1_dec_raw(&pbuf_stream, tlv.value_len, request->community, &request->community_strlen, SNMP_MAX_COMMUNITY_STR_LEN);

		if(err == ERR_MEM) {
			/* community string does not fit in our buffer -> its too long -> its invalid */
			request->community_strlen = 0;
			snmp_pbuf_stream_seek(&pbuf_stream, tlv.value_len);
		} else {
			IF_PARSE_ASSERT(err == ERR_OK);
		}

		/* add zero terminator */
		request->community[request->community_strlen] = 0;
	}

	/* decode PDU type (next container level) */
	IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
	IF_PARSE_ASSERT(tlv.value_len <= pbuf_stream.length);
	request->inbound_padding_len = pbuf_stream.length - tlv.value_len;
	parent_tlv_value_len = tlv.value_len;

	/* validate PDU type */
	switch(tlv.type) {
		case(SNMP_ASN1_CLASS_CONTEXT | SNMP_ASN1_CONTENTTYPE_CONSTRUCTED | SNMP_ASN1_CONTEXT_PDU_GET_REQ):
			/* GetRequest PDU */
			snmp_stats.ingetrequests++;
			break;

		case(SNMP_ASN1_CLASS_CONTEXT | SNMP_ASN1_CONTENTTYPE_CONSTRUCTED | SNMP_ASN1_CONTEXT_PDU_GET_NEXT_REQ):
			/* GetNextRequest PDU */
			snmp_stats.ingetnexts++;
			break;

		case(SNMP_ASN1_CLASS_CONTEXT | SNMP_ASN1_CONTENTTYPE_CONSTRUCTED | SNMP_ASN1_CONTEXT_PDU_GET_BULK_REQ):

			/* GetBulkRequest PDU */
			if(request->version < SNMP_VERSION_2c) {
				/* RFC2089: invalid, drop packet */
				return ERR_ARG;
			}

			break;

		case(SNMP_ASN1_CLASS_CONTEXT | SNMP_ASN1_CONTENTTYPE_CONSTRUCTED | SNMP_ASN1_CONTEXT_PDU_SET_REQ):
			/* SetRequest PDU */
			snmp_stats.insetrequests++;
			break;

		default:
			/* unsupported input PDU for this agent (no parse error) */
			LWIP_DEBUGF(SNMP_DEBUG, ("Unknown/Invalid SNMP PDU type received: %d", tlv.type));
			\
			return ERR_ARG;
			break;
	}

	request->request_type = tlv.type & SNMP_ASN1_DATATYPE_MASK;

	/* validate community (do this after decoding PDU type because we don't want to increase 'inbadcommunitynames' for wrong frame types */
	if(request->community_strlen == 0) {
		/* community string was too long or really empty*/
		snmp_stats.inbadcommunitynames++;
		snmp_authfail_trap();
		return ERR_ARG;
	} else if(request->request_type == SNMP_ASN1_CONTEXT_PDU_SET_REQ) {
		if(snmp_community_write[0] == 0) {
			/* our write community is empty, that means all our objects are readonly */
			request->error_status = SNMP_ERR_NOTWRITABLE;
			request->error_index  = 1;
		} else if(strncmp(snmp_community_write, (const char*)request->community, SNMP_MAX_COMMUNITY_STR_LEN) != 0) {
			/* community name does not match */
			snmp_stats.inbadcommunitynames++;
			snmp_authfail_trap();
			return ERR_ARG;
		}
	} else {
		if(strncmp(snmp_community, (const char*)request->community, SNMP_MAX_COMMUNITY_STR_LEN) != 0) {
			/* community name does not match */
			snmp_stats.inbadcommunitynames++;
			snmp_authfail_trap();
			return ERR_ARG;
		}
	}

	/* decode request ID */
	IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
	IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_INTEGER);
	parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
	IF_PARSE_ASSERT(parent_tlv_value_len > 0);

	IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &request->request_id));

	/* decode error status / non-repeaters */
	IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
	IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_INTEGER);
	parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
	IF_PARSE_ASSERT(parent_tlv_value_len > 0);

	if(request->request_type == SNMP_ASN1_CONTEXT_PDU_GET_BULK_REQ) {
		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &request->non_repeaters));

		if(request->non_repeaters < 0) {
			/* RFC 1905, 4.2.3 */
			request->non_repeaters = 0;
		}
	} else {
		/* only check valid value, don't touch 'request->error_status', maybe a response error status was already set to above; */
		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &s32_value));
		IF_PARSE_ASSERT(s32_value == SNMP_ERR_NOERROR);
	}

	/* decode error index / max-repetitions */
	IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
	IF_PARSE_ASSERT(tlv.type == SNMP_ASN1_TYPE_INTEGER);
	parent_tlv_value_len -= SNMP_ASN1_TLV_LENGTH(tlv);
	IF_PARSE_ASSERT(parent_tlv_value_len > 0);

	if(request->request_type == SNMP_ASN1_CONTEXT_PDU_GET_BULK_REQ) {
		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &request->max_repetitions));

		if(request->max_repetitions < 0) {
			/* RFC 1905, 4.2.3 */
			request->max_repetitions = 0;
		}
	} else {
		IF_PARSE_EXEC(snmp_asn1_dec_s32t(&pbuf_stream, tlv.value_len, &request->error_index));
		IF_PARSE_ASSERT(s32_value == 0);
	}

	/* decode varbind-list type (next container level) */
	IF_PARSE_EXEC(snmp_asn1_dec_tlv(&pbuf_stream, &tlv));
	IF_PARSE_ASSERT((tlv.type == SNMP_ASN1_TYPE_SEQUENCE) && (tlv.value_len <= pbuf_stream.length));

	request->inbound_varbind_offset = pbuf_stream.offset;
	request->inbound_varbind_len    = pbuf_stream.length - request->inbound_padding_len;
	snmp_vb_enumerator_init(&(request->inbound_varbind_enumerator), request->inbound_pbuf, request->inbound_varbind_offset, request->inbound_varbind_len);

	return ERR_OK;
}

#define OF_BUILD_EXEC(code) BUILD_EXEC(code, ERR_ARG)

static err_t
snmp_prepare_outbound_frame(struct snmp_request* request)
{
	struct snmp_asn1_tlv tlv;
	struct snmp_pbuf_stream* pbuf_stream = &(request->outbound_pbuf_stream);

	/* try allocating pbuf(s) for maximum response size */
	request->outbound_pbuf = pbuf_alloc(PBUF_TRANSPORT, 1472, PBUF_RAM);

	if(request->outbound_pbuf == NULL) {
		return ERR_MEM;
	}

	snmp_pbuf_stream_init(pbuf_stream, request->outbound_pbuf, 0, request->outbound_pbuf->tot_len);

	/* 'Message' sequence */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 3, 0);
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));

	/* version */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_INTEGER, 0, 0);
	snmp_asn1_enc_s32t_cnt(request->version, &tlv.value_len);
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
	OF_BUILD_EXEC(snmp_asn1_enc_s32t(pbuf_stream, tlv.value_len, request->version));

#if LWIP_SNMP_V3

	if(request->version < SNMP_VERSION_3) {
#endif
		/* community */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 0, request->community_strlen);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, request->community, request->community_strlen));
#if LWIP_SNMP_V3
	} else {
		const char* id;

		/* globalData */
		request->outbound_msg_global_data_offset = pbuf_stream->offset;
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 1, 0);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));

		/* msgID */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_INTEGER, 0, 1);
		snmp_asn1_enc_s32t_cnt(request->msg_id, &tlv.value_len);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_s32t(pbuf_stream, tlv.value_len, request->msg_id));

		/* msgMaxSize */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_INTEGER, 0, 1);
		snmp_asn1_enc_s32t_cnt(request->msg_max_size, &tlv.value_len);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_s32t(pbuf_stream, tlv.value_len, request->msg_max_size));

		/* msgFlags */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 0, 1);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, &request->msg_flags, 1));

		/* msgSecurityModel */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_INTEGER, 0, 1);
		snmp_asn1_enc_s32t_cnt(request->msg_security_model, &tlv.value_len);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_s32t(pbuf_stream, tlv.value_len, request->msg_security_model));

		/* end of msgGlobalData */
		request->outbound_msg_global_data_end = pbuf_stream->offset;

		/* msgSecurityParameters */
		request->outbound_msg_security_parameters_str_offset = pbuf_stream->offset;
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 1, 0);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));

		request->outbound_msg_security_parameters_seq_offset = pbuf_stream->offset;
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 1, 0);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));

		/* msgAuthoritativeEngineID */
		snmpv3_get_engine_id(&id, &request->msg_authoritative_engine_id_len);
		MEMCPY(request->msg_authoritative_engine_id, id, request->msg_authoritative_engine_id_len);
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 0, request->msg_authoritative_engine_id_len);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, request->msg_authoritative_engine_id, request->msg_authoritative_engine_id_len));

		request->msg_authoritative_engine_time = snmpv3_get_engine_time();
		request->msg_authoritative_engine_boots = snmpv3_get_engine_boots();

		/* msgAuthoritativeEngineBoots */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_INTEGER, 0, 0);
		snmp_asn1_enc_s32t_cnt(request->msg_authoritative_engine_boots, &tlv.value_len);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_s32t(pbuf_stream, tlv.value_len, request->msg_authoritative_engine_boots));

		/* msgAuthoritativeEngineTime */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_INTEGER, 0, 0);
		snmp_asn1_enc_s32t_cnt(request->msg_authoritative_engine_time, &tlv.value_len);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_s32t(pbuf_stream, tlv.value_len, request->msg_authoritative_engine_time));

		/* msgUserName */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 0, request->msg_user_name_len);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, request->msg_user_name, request->msg_user_name_len));

#if LWIP_SNMP_V3_CRYPTO

		/* msgAuthenticationParameters */
		if(request->msg_flags & SNMP_V3_AUTH_FLAG) {
			memset(request->msg_authentication_parameters, 0, SNMP_V3_MAX_AUTH_PARAM_LENGTH);
			request->outbound_msg_authentication_parameters_offset = pbuf_stream->offset;
			SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 1, SNMP_V3_MAX_AUTH_PARAM_LENGTH);
			OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
			OF_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, request->msg_authentication_parameters, SNMP_V3_MAX_AUTH_PARAM_LENGTH));
		} else
#endif
		{
			SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 0, 0);
			OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		}

#if LWIP_SNMP_V3_CRYPTO

		/* msgPrivacyParameters */
		if(request->msg_flags & SNMP_V3_PRIV_FLAG) {
			snmpv3_build_priv_param(request->msg_privacy_parameters);

			SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 0, SNMP_V3_MAX_PRIV_PARAM_LENGTH);
			OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
			OF_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, request->msg_privacy_parameters, SNMP_V3_MAX_PRIV_PARAM_LENGTH));
		} else
#endif
		{
			SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 0, 0);
			OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		}

		/* End of msgSecurityParameters, so we can calculate the length of this sequence later */
		request->outbound_msg_security_parameters_end = pbuf_stream->offset;

#if LWIP_SNMP_V3_CRYPTO

		/* For encryption we have to encapsulate the payload in an octet string */
		if(request->msg_flags & SNMP_V3_PRIV_FLAG) {
			request->outbound_scoped_pdu_string_offset = pbuf_stream->offset;
			SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 3, 0);
			OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		}

#endif
		/* Scoped PDU
		 * Encryption context
		 */
		request->outbound_scoped_pdu_seq_offset = pbuf_stream->offset;
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 3, 0);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));

		/* contextEngineID */
		snmpv3_get_engine_id(&id, &request->context_engine_id_len);
		MEMCPY(request->context_engine_id, id, request->context_engine_id_len);
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 0, request->context_engine_id_len);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, request->context_engine_id, request->context_engine_id_len));

		/* contextName */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 0, request->context_name_len);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, request->context_name, request->context_name_len));
	}

#endif

	/* 'PDU' sequence */
	request->outbound_pdu_offset = pbuf_stream->offset;
	SNMP_ASN1_SET_TLV_PARAMS(tlv, (SNMP_ASN1_CLASS_CONTEXT | SNMP_ASN1_CONTENTTYPE_CONSTRUCTED | SNMP_ASN1_CONTEXT_PDU_GET_RESP), 3, 0);
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));

	/* request ID */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_INTEGER, 0, 0);
	snmp_asn1_enc_s32t_cnt(request->request_id, &tlv.value_len);
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
	OF_BUILD_EXEC(snmp_asn1_enc_s32t(pbuf_stream, tlv.value_len, request->request_id));

	/* error status */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_INTEGER, 0, 1);
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
	request->outbound_error_status_offset = pbuf_stream->offset;
	OF_BUILD_EXEC(snmp_pbuf_stream_write(pbuf_stream, 0));

	/* error index */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_INTEGER, 0, 1);
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
	request->outbound_error_index_offset = pbuf_stream->offset;
	OF_BUILD_EXEC(snmp_pbuf_stream_write(pbuf_stream, 0));

	/* 'VarBindList' sequence */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 3, 0);
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));

	request->outbound_varbind_offset = pbuf_stream->offset;

	return ERR_OK;
}

/** Calculate the length of a varbind list */
err_t
snmp_varbind_length(struct snmp_varbind* varbind, struct snmp_varbind_len* len)
{
	/* calculate required lengths */
	snmp_asn1_enc_oid_cnt(varbind->oid.id, varbind->oid.len, &len->oid_value_len);
	snmp_asn1_enc_length_cnt(len->oid_value_len, &len->oid_len_len);

	if(varbind->value_len == 0) {
		len->value_value_len = 0;
	} else if(varbind->value_len & SNMP_GET_VALUE_RAW_DATA) {
		len->value_value_len = varbind->value_len & (~SNMP_GET_VALUE_RAW_DATA);
	} else {
		switch(varbind->type) {
			case SNMP_ASN1_TYPE_INTEGER:
				if(varbind->value_len != sizeof(s32_t)) {
					return ERR_VAL;
				}

				snmp_asn1_enc_s32t_cnt(*((s32_t*) varbind->value), &len->value_value_len);
				break;

			case SNMP_ASN1_TYPE_COUNTER:
			case SNMP_ASN1_TYPE_GAUGE:
			case SNMP_ASN1_TYPE_TIMETICKS:
				if(varbind->value_len != sizeof(u32_t)) {
					return ERR_VAL;
				}

				snmp_asn1_enc_u32t_cnt(*((u32_t*) varbind->value), &len->value_value_len);
				break;

			case SNMP_ASN1_TYPE_OCTET_STRING:
			case SNMP_ASN1_TYPE_IPADDR:
			case SNMP_ASN1_TYPE_OPAQUE:
				len->value_value_len = varbind->value_len;
				break;

			case SNMP_ASN1_TYPE_NULL:
				if(varbind->value_len != 0) {
					return ERR_VAL;
				}

				len->value_value_len = 0;
				break;

			case SNMP_ASN1_TYPE_OBJECT_ID:
				if((varbind->value_len & 0x03) != 0) {
					return ERR_VAL;
				}

				snmp_asn1_enc_oid_cnt((u32_t*) varbind->value, varbind->value_len >> 2, &len->value_value_len);
				break;

			case SNMP_ASN1_TYPE_COUNTER64:
				if(varbind->value_len != (2 * sizeof(u32_t))) {
					return ERR_VAL;
				}

				snmp_asn1_enc_u64t_cnt((u32_t*) varbind->value, &len->value_value_len);
				break;

			default:
				/* unsupported type */
				return ERR_VAL;
		}
	}

	snmp_asn1_enc_length_cnt(len->value_value_len, &len->value_len_len);

	len->vb_value_len = 1 + len->oid_len_len + len->oid_value_len + 1 + len->value_len_len + len->value_value_len;
	snmp_asn1_enc_length_cnt(len->vb_value_len, &len->vb_len_len);

	return ERR_OK;
}

#define OVB_BUILD_EXEC(code) BUILD_EXEC(code, ERR_ARG)

err_t
snmp_append_outbound_varbind(struct snmp_pbuf_stream* pbuf_stream, struct snmp_varbind* varbind)
{
	struct snmp_asn1_tlv tlv;
	struct snmp_varbind_len len;
	err_t err;

	err = snmp_varbind_length(varbind, &len);

	if(err != ERR_OK) {
		return err;
	}

	/* check length already before adding first data because in case of GetBulk,
	 *  data added so far is returned and therefore no partial data shall be added
	 */
	if((1 + len.vb_len_len + len.vb_value_len) > pbuf_stream->length) {
		return ERR_BUF;
	}

	/* 'VarBind' sequence */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, len.vb_len_len, len.vb_value_len);
	OVB_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));

	/* VarBind OID */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OBJECT_ID, len.oid_len_len, len.oid_value_len);
	OVB_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));
	OVB_BUILD_EXEC(snmp_asn1_enc_oid(pbuf_stream, varbind->oid.id, varbind->oid.len));

	/* VarBind value */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, varbind->type, len.value_len_len, len.value_value_len);
	OVB_BUILD_EXEC(snmp_ans1_enc_tlv(pbuf_stream, &tlv));

	if(len.value_value_len > 0) {
		if(varbind->value_len & SNMP_GET_VALUE_RAW_DATA) {
			OVB_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, (u8_t*) varbind->value, len.value_value_len));
		} else {
			switch(varbind->type) {
				case SNMP_ASN1_TYPE_INTEGER:
					OVB_BUILD_EXEC(snmp_asn1_enc_s32t(pbuf_stream, len.value_value_len, *((s32_t*) varbind->value)));
					break;

				case SNMP_ASN1_TYPE_COUNTER:
				case SNMP_ASN1_TYPE_GAUGE:
				case SNMP_ASN1_TYPE_TIMETICKS:
					OVB_BUILD_EXEC(snmp_asn1_enc_u32t(pbuf_stream, len.value_value_len, *((u32_t*) varbind->value)));
					break;

				case SNMP_ASN1_TYPE_OCTET_STRING:
				case SNMP_ASN1_TYPE_IPADDR:
				case SNMP_ASN1_TYPE_OPAQUE:
					OVB_BUILD_EXEC(snmp_asn1_enc_raw(pbuf_stream, (u8_t*) varbind->value, len.value_value_len));
					len.value_value_len = varbind->value_len;
					break;

				case SNMP_ASN1_TYPE_OBJECT_ID:
					OVB_BUILD_EXEC(snmp_asn1_enc_oid(pbuf_stream, (u32_t*) varbind->value, varbind->value_len / sizeof(u32_t)));
					break;

				case SNMP_ASN1_TYPE_COUNTER64:
					OVB_BUILD_EXEC(snmp_asn1_enc_u64t(pbuf_stream, len.value_value_len, (u32_t*) varbind->value));
					break;

				default:
					LWIP_ASSERT("Unknown variable type", 0);
					break;
			}
		}
	}

	return ERR_OK;
}

static err_t
snmp_complete_outbound_frame(struct snmp_request* request)
{
	struct snmp_asn1_tlv tlv;
	u16_t frame_size;
	u8_t outbound_padding = 0;

	if(request->version == SNMP_VERSION_1) {
		if(request->error_status != SNMP_ERR_NOERROR) {
			/* map v2c error codes to v1 compliant error code (according to RFC 2089) */
			switch(request->error_status) {
				/* mapping of implementation specific "virtual" error codes
				 * (during processing of frame we already stored them in error_status field,
				 * so no need to check all varbinds here for those exceptions as suggested by RFC) */
				case SNMP_ERR_NOSUCHINSTANCE:
				case SNMP_ERR_NOSUCHOBJECT:
				case SNMP_ERR_ENDOFMIBVIEW:
					request->error_status = SNMP_ERR_NOSUCHNAME;
					break;

				/* mapping according to RFC */
				case SNMP_ERR_WRONGVALUE:
				case SNMP_ERR_WRONGENCODING:
				case SNMP_ERR_WRONGTYPE:
				case SNMP_ERR_WRONGLENGTH:
				case SNMP_ERR_INCONSISTENTVALUE:
					request->error_status = SNMP_ERR_BADVALUE;
					break;

				case SNMP_ERR_NOACCESS:
				case SNMP_ERR_NOTWRITABLE:
				case SNMP_ERR_NOCREATION:
				case SNMP_ERR_INCONSISTENTNAME:
				case SNMP_ERR_AUTHORIZATIONERROR:
					request->error_status = SNMP_ERR_NOSUCHNAME;
					break;

				case SNMP_ERR_RESOURCEUNAVAILABLE:
				case SNMP_ERR_COMMITFAILED:
				case SNMP_ERR_UNDOFAILED:
				default:
					request->error_status = SNMP_ERR_GENERROR;
					break;
			}
		}
	} else {
		if(request->request_type == SNMP_ASN1_CONTEXT_PDU_SET_REQ) {
			/* map error codes to according to RFC 1905 (4.2.5.  The SetRequest-PDU) return 'NotWritable' for unknown OIDs) */
			switch(request->error_status) {
				case SNMP_ERR_NOSUCHINSTANCE:
				case SNMP_ERR_NOSUCHOBJECT:
				case SNMP_ERR_ENDOFMIBVIEW:
					request->error_status = SNMP_ERR_NOTWRITABLE;
					break;

				default:
					break;
			}
		}

		if(request->error_status >= SNMP_VARBIND_EXCEPTION_OFFSET) {
			/* should never occur because v2 frames store exceptions directly inside varbinds and not as frame error_status */
			LWIP_DEBUGF(SNMP_DEBUG, ("snmp_complete_outbound_frame() > Found v2 request with varbind exception code stored as error status!\n"));
			return ERR_ARG;
		}
	}

	if((request->error_status != SNMP_ERR_NOERROR) || (request->request_type == SNMP_ASN1_CONTEXT_PDU_SET_REQ)) {
		/* all inbound vars are returned in response without any modification for error responses and successful set requests*/
		struct snmp_pbuf_stream inbound_stream;
		OF_BUILD_EXEC(snmp_pbuf_stream_init(&inbound_stream, request->inbound_pbuf, request->inbound_varbind_offset, request->inbound_varbind_len));
		OF_BUILD_EXEC(snmp_pbuf_stream_init(&(request->outbound_pbuf_stream), request->outbound_pbuf, request->outbound_varbind_offset, request->outbound_pbuf->tot_len - request->outbound_varbind_offset));
		snmp_pbuf_stream_writeto(&inbound_stream, &(request->outbound_pbuf_stream), 0);
	}

	frame_size = request->outbound_pbuf_stream.offset;

#if LWIP_SNMP_V3 && LWIP_SNMP_V3_CRYPTO

	/* Calculate padding for encryption */
	if(request->version == SNMP_VERSION_3 && (request->msg_flags & SNMP_V3_PRIV_FLAG)) {
		u8_t i;
		outbound_padding = (8 - (u8_t)((frame_size - request->outbound_scoped_pdu_seq_offset) & 0x07)) & 0x07;

		for(i = 0; i < outbound_padding; i++) {
			snmp_pbuf_stream_write(&request->outbound_pbuf_stream, 0);
		}
	}

#endif

	/* complete missing length in 'Message' sequence ; 'Message' tlv is located at the beginning (offset 0) */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 3, frame_size + outbound_padding - 1 - 3); /* - type - length_len(fixed, see snmp_prepare_outbound_frame()) */
	OF_BUILD_EXEC(snmp_pbuf_stream_init(&(request->outbound_pbuf_stream), request->outbound_pbuf, 0, request->outbound_pbuf->tot_len));
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(&(request->outbound_pbuf_stream), &tlv));

#if LWIP_SNMP_V3

	if(request->version == SNMP_VERSION_3) {
		/* complete missing length in 'globalData' sequence */
		/* - type - length_len(fixed, see snmp_prepare_outbound_frame()) */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 1, request->outbound_msg_global_data_end
		                         - request->outbound_msg_global_data_offset - 1 - 1);
		OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&(request->outbound_pbuf_stream), request->outbound_msg_global_data_offset));
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(&(request->outbound_pbuf_stream), &tlv));

		/* complete missing length in 'msgSecurityParameters' sequence */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 1, request->outbound_msg_security_parameters_end
		                         - request->outbound_msg_security_parameters_str_offset - 1 - 1);
		OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&(request->outbound_pbuf_stream), request->outbound_msg_security_parameters_str_offset));
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(&(request->outbound_pbuf_stream), &tlv));

		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 1, request->outbound_msg_security_parameters_end
		                         - request->outbound_msg_security_parameters_seq_offset - 1 - 1);
		OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&(request->outbound_pbuf_stream), request->outbound_msg_security_parameters_seq_offset));
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(&(request->outbound_pbuf_stream), &tlv));

		/* complete missing length in scoped PDU sequence */
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 3, frame_size - request->outbound_scoped_pdu_seq_offset - 1 - 3);
		OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&(request->outbound_pbuf_stream), request->outbound_scoped_pdu_seq_offset));
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(&(request->outbound_pbuf_stream), &tlv));
	}

#endif

	/* complete missing length in 'PDU' sequence */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, (SNMP_ASN1_CLASS_CONTEXT | SNMP_ASN1_CONTENTTYPE_CONSTRUCTED | SNMP_ASN1_CONTEXT_PDU_GET_RESP), 3,
	                         frame_size - request->outbound_pdu_offset - 1 - 3); /* - type - length_len(fixed, see snmp_prepare_outbound_frame()) */
	OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&(request->outbound_pbuf_stream), request->outbound_pdu_offset));
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(&(request->outbound_pbuf_stream), &tlv));

	/* process and encode final error status */
	if(request->error_status != 0) {
		u16_t len;
		snmp_asn1_enc_s32t_cnt(request->error_status, &len);

		if(len != 1) {
			/* error, we only reserved one byte for it */
			return ERR_ARG;
		}

		OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&(request->outbound_pbuf_stream), request->outbound_error_status_offset));
		OF_BUILD_EXEC(snmp_asn1_enc_s32t(&(request->outbound_pbuf_stream), len, request->error_status));

		/* for compatibility to v1, log statistics; in v2 (RFC 1907) these statistics are obsoleted */
		switch(request->error_status) {
			case SNMP_ERR_TOOBIG:
				snmp_stats.outtoobigs++;
				break;

			case SNMP_ERR_NOSUCHNAME:
				snmp_stats.outnosuchnames++;
				break;

			case SNMP_ERR_BADVALUE:
				snmp_stats.outbadvalues++;
				break;

			case SNMP_ERR_GENERROR:
			default:
				snmp_stats.outgenerrs++;
				break;
		}

		if(request->error_status == SNMP_ERR_TOOBIG) {
			request->error_index = 0; /* defined by RFC 1157 */
		} else if(request->error_index == 0) {
			/* set index to varbind where error occured (if not already set before, e.g. during GetBulk processing) */
			request->error_index = request->inbound_varbind_enumerator.varbind_count;
		}
	} else {
		if(request->request_type == SNMP_ASN1_CONTEXT_PDU_SET_REQ) {
			snmp_stats.intotalsetvars += request->inbound_varbind_enumerator.varbind_count;
		} else {
			snmp_stats.intotalreqvars += request->inbound_varbind_enumerator.varbind_count;
		}
	}

	/* encode final error index*/
	if(request->error_index != 0) {
		u16_t len;
		snmp_asn1_enc_s32t_cnt(request->error_index, &len);

		if(len != 1) {
			/* error, we only reserved one byte for it */
			return ERR_VAL;
		}

		OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&(request->outbound_pbuf_stream), request->outbound_error_index_offset));
		OF_BUILD_EXEC(snmp_asn1_enc_s32t(&(request->outbound_pbuf_stream), len, request->error_index));
	}

	/* complete missing length in 'VarBindList' sequence ; 'VarBindList' tlv is located directly before varbind offset */
	SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_SEQUENCE, 3, frame_size - request->outbound_varbind_offset);
	OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&(request->outbound_pbuf_stream), request->outbound_varbind_offset - 1 - 3));   /* - type - length_len(fixed, see snmp_prepare_outbound_frame()) */
	OF_BUILD_EXEC(snmp_ans1_enc_tlv(&(request->outbound_pbuf_stream), &tlv));

	/* Authenticate response */
#if LWIP_SNMP_V3 && LWIP_SNMP_V3_CRYPTO

	/* Encrypt response */
	if(request->version == SNMP_VERSION_3 && (request->msg_flags & SNMP_V3_PRIV_FLAG)) {
		u8_t key[20];
		u8_t algo;

		/* complete missing length in PDU sequence */
		OF_BUILD_EXEC(snmp_pbuf_stream_init(&request->outbound_pbuf_stream, request->outbound_pbuf, 0, request->outbound_pbuf->tot_len));
		OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&(request->outbound_pbuf_stream), request->outbound_scoped_pdu_string_offset));
		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 3, frame_size + outbound_padding
		                         - request->outbound_scoped_pdu_string_offset - 1 - 3);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(&(request->outbound_pbuf_stream), &tlv));

		OF_BUILD_EXEC(snmpv3_get_user((char*)request->msg_user_name, NULL, NULL, &algo, key));

		OF_BUILD_EXEC(snmpv3_crypt(&request->outbound_pbuf_stream, tlv.value_len, key,
		                           request->msg_privacy_parameters, request->msg_authoritative_engine_boots,
		                           request->msg_authoritative_engine_time, algo, SNMP_V3_PRIV_MODE_ENCRYPT));
	}

	if(request->version == SNMP_VERSION_3 && (request->msg_flags & SNMP_V3_AUTH_FLAG)) {
		u8_t key[20];
		u8_t algo;
		u8_t hmac[20];

		OF_BUILD_EXEC(snmpv3_get_user((char*)request->msg_user_name, &algo, key, NULL, NULL));
		OF_BUILD_EXEC(snmp_pbuf_stream_init(&(request->outbound_pbuf_stream),
		                                    request->outbound_pbuf, 0, request->outbound_pbuf->tot_len));
		OF_BUILD_EXEC(snmpv3_auth(&request->outbound_pbuf_stream, frame_size + outbound_padding, key, algo, hmac));

		MEMCPY(request->msg_authentication_parameters, hmac, SNMP_V3_MAX_AUTH_PARAM_LENGTH);
		OF_BUILD_EXEC(snmp_pbuf_stream_init(&request->outbound_pbuf_stream,
		                                    request->outbound_pbuf, 0, request->outbound_pbuf->tot_len));
		OF_BUILD_EXEC(snmp_pbuf_stream_seek_abs(&request->outbound_pbuf_stream,
		                                        request->outbound_msg_authentication_parameters_offset));

		SNMP_ASN1_SET_TLV_PARAMS(tlv, SNMP_ASN1_TYPE_OCTET_STRING, 1, SNMP_V3_MAX_AUTH_PARAM_LENGTH);
		OF_BUILD_EXEC(snmp_ans1_enc_tlv(&request->outbound_pbuf_stream, &tlv));
		OF_BUILD_EXEC(snmp_asn1_enc_raw(&request->outbound_pbuf_stream,
		                                request->msg_authentication_parameters, SNMP_V3_MAX_AUTH_PARAM_LENGTH));
	}

#endif

	pbuf_realloc(request->outbound_pbuf, frame_size + outbound_padding);

	snmp_stats.outgetresponses++;
	snmp_stats.outpkts++;

	return ERR_OK;
}

static void
snmp_execute_write_callbacks(struct snmp_request* request)
{
	struct snmp_varbind_enumerator inbound_varbind_enumerator;
	struct snmp_varbind vb;

	snmp_vb_enumerator_init(&inbound_varbind_enumerator, request->inbound_pbuf, request->inbound_varbind_offset, request->inbound_varbind_len);
	vb.value = NULL; /* do NOT decode value (we enumerate outbound buffer here, so all varbinds have values assigned, which we don't need here) */

	while(snmp_vb_enumerator_get_next(&inbound_varbind_enumerator, &vb) == SNMP_VB_ENUMERATOR_ERR_OK) {
		snmp_write_callback(vb.oid.id, vb.oid.len, snmp_write_callback_arg);
	}
}


/* ----------------------------------------------------------------------- */
/* VarBind enumerator methods */
/* ----------------------------------------------------------------------- */

void
snmp_vb_enumerator_init(struct snmp_varbind_enumerator* enumerator, struct pbuf* p, u16_t offset, u16_t length)
{
	snmp_pbuf_stream_init(&(enumerator->pbuf_stream), p, offset, length);
	enumerator->varbind_count = 0;
}

#define VB_PARSE_EXEC(code)   PARSE_EXEC(code, SNMP_VB_ENUMERATOR_ERR_ASN1ERROR)
#define VB_PARSE_ASSERT(code) PARSE_ASSERT(code, SNMP_VB_ENUMERATOR_ERR_ASN1ERROR)

snmp_vb_enumerator_err_t
snmp_vb_enumerator_get_next(struct snmp_varbind_enumerator* enumerator, struct snmp_varbind* varbind)
{
	struct snmp_asn1_tlv tlv;
	u16_t  varbind_len;
	err_t  err;

	if(enumerator->pbuf_stream.length == 0) {
		return SNMP_VB_ENUMERATOR_ERR_EOVB;
	}

	enumerator->varbind_count++;

	/* decode varbind itself (parent container of a varbind) */
	VB_PARSE_EXEC(snmp_asn1_dec_tlv(&(enumerator->pbuf_stream), &tlv));
	VB_PARSE_ASSERT((tlv.type == SNMP_ASN1_TYPE_SEQUENCE) && (tlv.value_len <= enumerator->pbuf_stream.length));
	varbind_len = tlv.value_len;

	/* decode varbind name (object id) */
	VB_PARSE_EXEC(snmp_asn1_dec_tlv(&(enumerator->pbuf_stream), &tlv));
	VB_PARSE_ASSERT((tlv.type == SNMP_ASN1_TYPE_OBJECT_ID) && (SNMP_ASN1_TLV_LENGTH(tlv) < varbind_len) && (tlv.value_len < enumerator->pbuf_stream.length));

	VB_PARSE_EXEC(snmp_asn1_dec_oid(&(enumerator->pbuf_stream), tlv.value_len, varbind->oid.id, &(varbind->oid.len), SNMP_MAX_OBJ_ID_LEN));
	varbind_len -= SNMP_ASN1_TLV_LENGTH(tlv);

	/* decode varbind value (object id) */
	VB_PARSE_EXEC(snmp_asn1_dec_tlv(&(enumerator->pbuf_stream), &tlv));
	VB_PARSE_ASSERT((SNMP_ASN1_TLV_LENGTH(tlv) == varbind_len) && (tlv.value_len <= enumerator->pbuf_stream.length));
	varbind->type = tlv.type;

	/* shall the value be decoded ? */
	if(varbind->value != NULL) {
		switch(varbind->type) {
			case SNMP_ASN1_TYPE_INTEGER:
				VB_PARSE_EXEC(snmp_asn1_dec_s32t(&(enumerator->pbuf_stream), tlv.value_len, (s32_t*)varbind->value));
				varbind->value_len = sizeof(s32_t*);
				break;

			case SNMP_ASN1_TYPE_COUNTER:
			case SNMP_ASN1_TYPE_GAUGE:
			case SNMP_ASN1_TYPE_TIMETICKS:
				VB_PARSE_EXEC(snmp_asn1_dec_u32t(&(enumerator->pbuf_stream), tlv.value_len, (u32_t*)varbind->value));
				varbind->value_len = sizeof(u32_t*);
				break;

			case SNMP_ASN1_TYPE_OCTET_STRING:
			case SNMP_ASN1_TYPE_OPAQUE:
				err = snmp_asn1_dec_raw(&(enumerator->pbuf_stream), tlv.value_len, (u8_t*)varbind->value, &varbind->value_len, SNMP_MAX_VALUE_SIZE);

				if(err == ERR_MEM) {
					return SNMP_VB_ENUMERATOR_ERR_INVALIDLENGTH;
				}

				VB_PARSE_ASSERT(err == ERR_OK);
				break;

			case SNMP_ASN1_TYPE_NULL:
				varbind->value_len = 0;
				break;

			case SNMP_ASN1_TYPE_OBJECT_ID:
				/* misuse tlv.length_len as OID_length transporter */
				err = snmp_asn1_dec_oid(&(enumerator->pbuf_stream), tlv.value_len, (u32_t*)varbind->value, &tlv.length_len, SNMP_MAX_OBJ_ID_LEN);

				if(err == ERR_MEM) {
					return SNMP_VB_ENUMERATOR_ERR_INVALIDLENGTH;
				}

				VB_PARSE_ASSERT(err == ERR_OK);
				varbind->value_len = tlv.length_len * sizeof(u32_t);
				break;

			case SNMP_ASN1_TYPE_IPADDR:
				if(tlv.value_len == 4) {
					/* must be exactly 4 octets! */
					VB_PARSE_EXEC(snmp_asn1_dec_raw(&(enumerator->pbuf_stream), tlv.value_len, (u8_t*)varbind->value, &varbind->value_len, SNMP_MAX_VALUE_SIZE));
				} else {
					VB_PARSE_ASSERT(0);
				}

				break;

			case SNMP_ASN1_TYPE_COUNTER64:
				VB_PARSE_EXEC(snmp_asn1_dec_u64t(&(enumerator->pbuf_stream), tlv.value_len, (u32_t*)varbind->value));
				varbind->value_len = 2 * sizeof(u32_t*);
				break;

			default:
				VB_PARSE_ASSERT(0);
				break;
		}
	} else {
		snmp_pbuf_stream_seek(&(enumerator->pbuf_stream), tlv.value_len);
		varbind->value_len = tlv.value_len;
	}

	return SNMP_VB_ENUMERATOR_ERR_OK;
}

#endif /* LWIP_SNMP */
